Skip to content

feat: route all commands through Vercel Sandbox#7

Open
sweetmantech wants to merge 3 commits intomainfrom
sweetmantech/myc-4221-bash-all-commands-trigger-sandbox-no-split-logic-for-ls-or
Open

feat: route all commands through Vercel Sandbox#7
sweetmantech wants to merge 3 commits intomainfrom
sweetmantech/myc-4221-bash-all-commands-trigger-sandbox-no-split-logic-for-ls-or

Conversation

@sweetmantech
Copy link
Copy Markdown

@sweetmantech sweetmantech commented Feb 11, 2026

Summary

  • Add /api/exec endpoint that creates and reuses Vercel Sandbox VMs for command execution
  • Remove client-side just-bash/browser dependency — all terminal commands now execute server-side in a real sandbox
  • Refactor input-handler.ts to accept a generic exec function instead of the Bash type
  • Refactor agent-command.ts to export a standalone handler (removes defineCommand dependency)
  • No split logic: ls, cat, grep, and all other commands uniformly go through the sandbox

Test plan

  • Verify ls, cat, grep, and other bash commands execute correctly via sandbox
  • Verify agent command still works (streams response, multi-turn chat)
  • Verify static commands (about, install, github) return expected output
  • Verify clear still clears the terminal locally
  • Verify tab completion works (file names from sandbox ls -1)
  • Verify sandbox session persistence (sandboxId reused across commands)
  • Verify ?agent= query parameter still triggers agent command

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Commands now execute in an isolated sandbox environment with enhanced security.
    • Authorization via Bearer token required for command execution.
    • Agent command prompt processing integrated into terminal workflow.
    • Improved command routing with consistent error handling and session persistence.

Remove client-side just-bash/browser dependency for command execution.
All terminal commands now go through /api/exec which creates and reuses
a Vercel Sandbox VM, eliminating split logic between local and remote
command execution.

- Add /api/exec endpoint that creates/reuses sandbox sessions
- Refactor input-handler to accept generic exec function
- Refactor agent-command to standalone handler (no defineCommand)
- Update Terminal.tsx to route commands: static (local) → agent (API) → sandbox (API)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bash Ready Ready Preview Feb 11, 2026 9:34pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 11, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 29 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This change introduces a new server API endpoint for executing shell commands in sandboxes with Bearer token authentication and replaces client-side Bash execution with a unified routing layer that directs commands to static handlers, an agent handler, or the sandboxed server endpoint.

Changes

Cohort / File(s) Summary
API Sandbox Execution
app/api/exec/route.ts
New POST endpoint that validates Bearer tokens, executes bash commands in sandboxes, handles sandbox creation/reuse, and returns command output with exitCode and active sandboxId.
Terminal Command Routing
app/components/Terminal.tsx
Refactored to route commands through a new exec layer: static commands (about, install, github), agent command via agentHandler, and remaining commands to /api/exec server endpoint with sandboxId persistence and token-based auth.
Agent Handler Refactor
app/components/terminal-parts/agent-command.ts, app/components/terminal-parts/index.ts
Renamed createAgentCommand to createAgentHandler and changed return type from command object to a handler function (prompt: string) => Promise<ExecResult>, simplifying the entry point structure.
Executor Abstraction
app/components/terminal-parts/input-handler.ts
Replaced Bash dependency with generic ExecFn abstraction; createInputHandler now accepts exec: ExecFn instead of bash: Bash, with all bash.exec() calls replaced accordingly.

Sequence Diagram

sequenceDiagram
    participant User
    participant Terminal as Terminal Component
    participant Router as Command Router
    participant Agent as Agent Handler
    participant API as /api/exec Endpoint
    participant Sandbox as Sandbox Engine
    participant Auth as Auth Service

    User->>Terminal: Enter command
    Terminal->>Router: Route command
    
    alt Static Command
        Router->>Terminal: Execute locally (about, install, github)
    else Agent Command
        Router->>Agent: Delegate to agent handler
        Agent->>Auth: Validate auth token
        Auth-->>Agent: Token valid
        Agent->>API: Send agent request
        API->>Sandbox: Execute in sandbox
        Sandbox-->>API: Return output
        API-->>Agent: Response
        Agent-->>Terminal: ExecResult
    else Regular Command
        Router->>Auth: Check authorization
        Auth-->>Router: Token valid
        Router->>API: POST /api/exec with sandboxId
        API->>Sandbox: Fetch/create sandbox & execute
        Sandbox-->>API: Command output
        API-->>Router: JSON response (stdout, stderr, exitCode)
        Router-->>Terminal: ExecResult
    end
    
    Terminal->>User: Display output
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hops with glee through sandbox gates,
Commands route where agent waits,
Auth tokens flow, shells execute,
No client-side Bash™ repute!
Server-side dreams in /api/exec—
This bunny's happy with these checks! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: routing all commands through Vercel Sandbox, which is the central theme across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4221-bash-all-commands-trigger-sandbox-no-split-logic-for-ls-or

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@app/api/exec/route.ts`:
- Around line 61-65: The POST handler currently only checks that authHeader
starts with "Bearer " but never extracts or validates the token; update the POST
function to extract the token (const token = authHeader.slice(7).trim()) and
validate it before proceeding by either verifying a JWT (using your JWT
secret/public key or a library) or calling your identity provider/token
introspection endpoint; if verification fails, return a 401 Response.json({
error: "Unauthorized" }, { status: 401 }); keep all existing behavior for
authorized requests and ensure failures are short-circuited immediately in POST.
- Line 67: The code directly destructures the request body with `const {
command, sandboxId } = await req.json();` which throws on malformed JSON; wrap
the `await req.json()` call in a try/catch, handle JSON parse errors by
returning an HTTP 400 response (or calling the existing error responder) with a
clear message, and validate that `command` and `sandboxId` exist after parsing
before proceeding so bad or missing payloads are rejected gracefully.
- Around line 6-7: The code uses import.meta.url to build __dirname and compute
AGENT_DATA_DIR which can break when Next.js bundles API routes; change the
resolution to use a stable base (e.g., process.cwd() or a configured env var) to
compute the absolute path to the generated _agent-data folder instead of relying
on import.meta.url. Update the constants in route.ts (the __dirname and
AGENT_DATA_DIR definitions) to derive AGENT_DATA_DIR from process.cwd() or a
known APP_ROOT/env var and add fallback/error logging if the directory is
missing so runtime path resolution is robust in both dev and production.
- Around line 88-112: Add a per-command timeout using AbortController around the
sandbox.runCommand call: create an AbortController (e.g., ac), start a
setTimeout to call ac.abort() after the desired timeout (e.g., 30_000 ms), pass
ac.signal in the options to sandbox.runCommand (alongside
cmd/args/cwd/SANDBOX_CWD), and ensure you clearTimeout(timeout) in a finally
block so the timer is cleaned up; keep the existing stdout/stderr retrieval and
error handling (the catch can continue to return error.message or "Execution
failed").
🧹 Nitpick comments (4)
app/api/exec/route.ts (1)

10-32: Unbounded recursive file read with no size/count limits.

readSourceFiles recursively reads all files under AGENT_DATA_DIR into memory (as Buffers) with no cap on file count or total size. If the agent data directory grows, this could cause high memory usage or slow sandbox creation.

Consider adding a file count limit or total size cap, or at minimum logging a warning if the directory is unexpectedly large.

app/components/terminal-parts/agent-command.ts (1)

4-8: ExecResult type is duplicated across three files.

The same ExecResult type is defined in agent-command.ts, input-handler.ts, and Terminal.tsx. Consider extracting it to a shared types module (e.g., in terminal-parts/types.ts) and re-exporting it from the barrel file.

app/components/terminal-parts/input-handler.ts (1)

206-213: Tab completion triggers a sandbox API round-trip on every Tab press.

exec("ls -1") now makes a network request to /api/exec for file completion. This introduces noticeable latency compared to the previous client-side approach. Users who press Tab frequently may perceive the shell as sluggish.

Consider caching the ls -1 result for a short TTL (e.g., invalidate after the next command execution), or debouncing repeated Tab presses.

app/components/Terminal.tsx (1)

76-86: Minor: quote-stripping only handles balanced, non-nested quotes.

The quote removal on Lines 79–84 handles "..." and '...' but won't handle escaped quotes, mismatched quotes, or nested quotes. This is likely fine for a terminal prompt input, but worth noting.

Comment on lines +61 to +65
export async function POST(req: Request) {
const authHeader = req.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bearer token is accepted but never validated.

The auth check on Line 63 only verifies the Authorization header starts with "Bearer " but never extracts or validates the actual token value. Any request with Authorization: Bearer anything will pass. Since this endpoint executes arbitrary shell commands in a sandbox, this is effectively an unauthenticated command-execution endpoint.

Consider verifying the token against your auth provider (e.g., decode/verify a JWT, or check against an identity provider).

🤖 Prompt for AI Agents
In `@app/api/exec/route.ts` around lines 61 - 65, The POST handler currently only
checks that authHeader starts with "Bearer " but never extracts or validates the
token; update the POST function to extract the token (const token =
authHeader.slice(7).trim()) and validate it before proceeding by either
verifying a JWT (using your JWT secret/public key or a library) or calling your
identity provider/token introspection endpoint; if verification fails, return a
401 Response.json({ error: "Unauthorized" }, { status: 401 }); keep all existing
behavior for authorized requests and ensure failures are short-circuited
immediately in POST.

return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const { command, sandboxId } = await req.json();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unhandled JSON parse error on malformed request body.

await req.json() will throw if the body is not valid JSON, resulting in an unhandled exception and a 500 response instead of a clean 400 error.

Proposed fix
-  const { command, sandboxId } = await req.json();
+  let command: string | undefined;
+  let sandboxId: string | undefined;
+  try {
+    ({ command, sandboxId } = await req.json());
+  } catch {
+    return Response.json({ error: "Invalid JSON body" }, { status: 400 });
+  }
🤖 Prompt for AI Agents
In `@app/api/exec/route.ts` at line 67, The code directly destructures the request
body with `const { command, sandboxId } = await req.json();` which throws on
malformed JSON; wrap the `await req.json()` call in a try/catch, handle JSON
parse errors by returning an HTTP 400 response (or calling the existing error
responder) with a clear message, and validate that `command` and `sandboxId`
exist after parsing before proceeding so bad or missing payloads are rejected
gracefully.

Comment on lines +88 to +112

try {
const result = await sandbox.runCommand({
cmd: "bash",
args: ["-c", command],
cwd: SANDBOX_CWD,
});

const stdout = await result.stdout();
const stderr = await result.stderr();

return Response.json({
stdout,
stderr,
exitCode: result.exitCode,
sandboxId: activeSandboxId,
});
} catch (error) {
return Response.json({
stdout: "",
stderr: error instanceof Error ? error.message : "Execution failed",
exitCode: 1,
sandboxId: activeSandboxId,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@vercel/sandbox runCommand timeout option documentation

💡 Result:

@vercel/sandbox does not document a timeout option on sandbox.runCommand(...). The runCommand API supports cancellation via an AbortSignal (opts.signal / params.signal), but no per-command timeout field is listed. [1]

Timeout is configured at the sandbox level instead:

  • Set initial sandbox lifetime with timeout when calling Sandbox.create({ timeout: ... }) (milliseconds). [1][2]
  • Extend a running sandbox with sandbox.extendTimeout(...). [1][2]
  • By default, a sandbox times out after 5 minutes (plan maximums apply). [2]

Example (per-command “timeout” via AbortController):

const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 30_000);

try {
  await sandbox.runCommand({ cmd: "bash", args: ["-lc", "sleep 999"], signal: ac.signal });
} finally {
  clearTimeout(t);
}

Sources: [1] [2]


🏁 Script executed:

# Find the file and examine the full context around lines 88-112
fd -t f "route.ts" | grep -E "app/api/exec" | head -5

Repository: recoupable/bash

Length of output: 80


🏁 Script executed:

# Search for sandbox initialization and configuration
rg -A 5 -B 5 "sandbox.runCommand" app/

Repository: recoupable/bash

Length of output: 1778


🏁 Script executed:

cat -n app/api/exec/route.ts | head -90

Repository: recoupable/bash

Length of output: 3389


🏁 Script executed:

# Search for the sandbox creation function
rg -A 20 "createAndSeedSandbox" app/api/exec/route.ts

Repository: recoupable/bash

Length of output: 1434


Add per-command timeout using AbortSignal to prevent hanging commands.

Currently, sandbox.runCommand() at line 90 has no timeout protection. While the sandbox has a default 5-minute lifetime, individual commands can hang indefinitely within that window (e.g., cat with no file, sleep 999999, infinite loops). The @vercel/sandbox SDK supports cancellation via AbortSignal—use AbortController to implement a per-command timeout:

Example fix
const ac = new AbortController();
const timeout = setTimeout(() => ac.abort(), 30_000); // 30s timeout

try {
  const result = await sandbox.runCommand({
    cmd: "bash",
    args: ["-c", command],
    cwd: SANDBOX_CWD,
    signal: ac.signal,
  });
  // ... rest of code
} finally {
  clearTimeout(timeout);
}
🤖 Prompt for AI Agents
In `@app/api/exec/route.ts` around lines 88 - 112, Add a per-command timeout using
AbortController around the sandbox.runCommand call: create an AbortController
(e.g., ac), start a setTimeout to call ac.abort() after the desired timeout
(e.g., 30_000 ms), pass ac.signal in the options to sandbox.runCommand
(alongside cmd/args/cwd/SANDBOX_CWD), and ensure you clearTimeout(timeout) in a
finally block so the timer is cleaned up; keep the existing stdout/stderr
retrieval and error handling (the catch can continue to return error.message or
"Execution failed").

The previous approach read _agent-data from disk via __dirname, which
doesn't work in serverless since each route is bundled independently.
Now fetches files via the existing /api/fs endpoint. Added top-level
try/catch so errors return useful messages instead of raw 500s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DRY: Move readSourceFiles and createSandbox into app/api/_lib/ so both
/api/agent and /api/exec reuse the same sandbox creation logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant